package top.yangguangmc.qq_login_helper;

import javax.swing.*;
import java.awt.*;
import java.awt.event.InputEvent;
import java.io.*;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.stream.Collectors;

import static java.awt.event.KeyEvent.*;

public final class QQLoginHelper {
    private final Properties properties;
    private final Robot robot;
    private final boolean debug;
    private boolean useInputDelay = false;
    private volatile ProcessState processState = ProcessState.PREPARING;
    private final int startUpDelay, genericDelay, inputDelay;

    public QQLoginHelper() throws AWTException, IOException {
        System.out.println("Initializing...");
        this.properties = new Properties();
        this.robot = new Robot();
        System.out.println("Reading config...");
        URL defaultFile = QQLoginHelper.class.getResource("config.properties");
        if (defaultFile == null) throw new AssertionError("Default config.properties not found.");
        File file = new File(System.getProperty("user.dir") + "\\config.properties");
        boolean hasChanges = false;
        if (!file.exists()) {
            System.out.println("Creating default config...");
            try (InputStream is = defaultFile.openStream()) {
                Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
            }
        }
        try (InputStream is = defaultFile.openStream(); Reader reader = new BufferedReader(new FileReader(file))) {
            Properties defaultProp = new Properties();
            defaultProp.load(is);
            properties.load(reader);
            for (Object o : defaultProp.keySet()) {
                String key = (String) o;
                if (!properties.containsKey(key)) {
                    System.err.println("Cannot find property '" + key + "'! Creating one...");
                    properties.setProperty(key, defaultProp.getProperty(key));
                    hasChanges = true;
                }
            }
        }
        if (hasChanges) {
            try (Writer writer = new BufferedWriter(new FileWriter(file))) {
                properties.store(writer, "Configuration file");
            }
        }
        this.debug = Boolean.parseBoolean(properties.getProperty("debug", "false"));
        try {
            startUpDelay = Integer.parseInt(properties.getProperty("delay.qq-startup"));
            genericDelay = Integer.parseInt(properties.getProperty("delay.generic"));
            inputDelay = Integer.parseInt(properties.getProperty("delay.input"));
            if (startUpDelay < 0 || genericDelay < 0 || inputDelay < 0) throw new NumberFormatException();
        } catch (NumberFormatException e) {
            throw new ConfigException("delay", "应为有效正整数");
        }
    }

    private void run() throws InterruptedException, IOException {
        System.out.println("Main process started.");
        String account = properties.getProperty("account");
        if (account.trim().isEmpty()) throw new ConfigException("account", "不能留空");
        String password = properties.getProperty("password");
        if (password.isEmpty()) throw new ConfigException("password", "不能留空");
        boolean tick = Boolean.parseBoolean(properties.getProperty("tick-privacy-rules"));
        UISituation situation;
        try {
            int ord = Integer.parseInt(properties.getProperty("ui-situation"));
            if (ord < 0 || ord >= UISituation.values().length)
                throw new ConfigException("ui-situation", "应该要在 0 ~ " + UISituation.values().length + " 之内");
            situation = UISituation.values()[ord];
        } catch (NumberFormatException e) {
            try {
                situation = UISituation.valueOf(properties.getProperty("ui-situation"));
            } catch (IllegalArgumentException e2) {
                throw new ConfigException("ui-situation", "应为下列之一："
                        + Arrays.stream(UISituation.values()).map(Object::toString).collect(Collectors.joining(", ")));
            }
        }
        int clickX, clickY;
        try {
            clickX = Integer.parseInt(properties.getProperty("click-pos.x"));
            clickY = Integer.parseInt(properties.getProperty("click-pos.y"));
        } catch (NumberFormatException e) {
            throw new ConfigException("click-pos", "应为有效整数");
        }

        delay();
        String exe = debug ? "notepad" : properties.getProperty("qq-path");
        System.out.println("Properties: ui-situation=" + situation + ", click=(" + clickX + ", " + clickY + "), exe='" + exe + "'.");
        long t1 = System.currentTimeMillis();
        System.out.println("Waiting for QQ to startup...");
        Process process = new ProcessBuilder(exe).start();
        Thread watcher = getWatcherThread(process);
        watcher.start();
        while (processState == ProcessState.PREPARING) {
            if (!process.isAlive()) processState = ProcessState.FAILED;
            if (System.currentTimeMillis() - t1 > startUpDelay) processState = ProcessState.TIMED_OUT;
            Thread.yield();
        }
        watcher.interrupt();
        if (processState == ProcessState.FAILED)
            throw new StartupFailedException();
        else if (processState == ProcessState.TIMED_OUT)
            System.err.println("QQ startup has took abnormally long. Ignoring it...");
        else if (processState == ProcessState.DONE)
            System.out.println("QQ startup finished.");


//        switchWindow();
//        delay();
//        System.out.println("Changing input method...");
//        InputContext context = InputContext.getInstance();
//        String inputMethod = context.getLocale().toString();
//        context.selectInputMethod(Locale.US);

        Thread.sleep(1000);
        switch (situation) {
            case QR_CODE:
                type(VK_TAB);
                delay();
                type(VK_TAB);
                delay();
                type(VK_ENTER);
                delay();
                break;
            case QR_CODE_SPECIAL:
                click(clickX, clickY);
                Thread.sleep(1000);
                type(VK_TAB);
                delay();
                break;
            case ACCOUNT_AND_PASSWORD:
                delay();
                type(VK_TAB);
                delay();
                break;
            default:
                throw new AssertionError("Case " + situation);
        }
        delay();
        type(VK_TAB);
        delay();
        System.out.println("Entering account...");
        enter(account);
        delay();
        type(VK_TAB);

        delay();
        System.out.println("Entering password...");
        enter(password);
        delay();
        type(VK_ENTER);
        delay();
        type(VK_ENTER);

        if (tick) {
            delay();
            type(VK_TAB);
            delay();
            type(VK_ENTER);
            delay();
            type(VK_TAB);
            delay();
            type(VK_TAB);
            delay();
            type(VK_TAB);
            delay();
            type(VK_ENTER);
        }

        delay();
//        context.selectInputMethod(Locale.forLanguageTag(inputMethod));
        System.out.println("Done in " + (System.currentTimeMillis() - t1) / 1000 + "s.");
    }

    private Thread getWatcherThread(Process process) {
        return new Thread(() -> {
            try (Scanner sc = new Scanner(process.getInputStream())) {
                while (processState == ProcessState.PREPARING) {
                    if (Thread.interrupted()) break;
                    if (!debug && sc.hasNextLine()) {
                        String s = sc.nextLine();
                        System.out.println("[QQ] " + s);
                        if (s.trim().equals("++++++registerSimMojoCallback+++++,") || s.trim().equals("[preload] register done. wrapper.node")) {
                            processState = ProcessState.DONE;
                            break;
                        }
                    }
                    Thread.yield();
                }
            }
        }, "ProcessWatcher");
    }

    private void delay() throws InterruptedException {
        Thread.sleep(useInputDelay ? inputDelay : genericDelay);
    }

    private void type(int key) throws InterruptedException {
        robot.keyPress(key);
        delay();
        robot.keyRelease(key);
    }

    private void enter(String s) throws InterruptedException {
        useInputDelay = true;
        for (char c : s.toCharArray()) {
            type(getExtendedKeyCodeForChar(c));
            delay();
        }
        useInputDelay = false;
    }

    private void click(int x, int y) throws InterruptedException {
        robot.mouseMove(x, y);
        delay();
        robot.mousePress(InputEvent.BUTTON1_DOWN_MASK);
        delay();
        robot.mouseRelease(InputEvent.BUTTON1_DOWN_MASK);
    }

//    private void switchWindow() throws InterruptedException {
//        robot.keyPress(VK_ALT);
//        delay();
//        robot.keyPress(VK_TAB);
//        delay();
//        robot.keyRelease(VK_TAB);
//        delay();
//        robot.keyRelease(VK_ALT);
//    }

    public static void main(String[] args) {
        try {
            try {
                QQLoginHelper instance = new QQLoginHelper();
                instance.run();
            } catch (ConfigException e) {
                System.err.println("Incorrect configuration!");
                e.printStackTrace();
                JOptionPane.showMessageDialog(null, e.getMessage(),
                        "配置文件不正确！", JOptionPane.ERROR_MESSAGE);
                System.exit(1);
            } catch (StartupFailedException e) {
                System.err.println("QQ startup failed!");
                e.printStackTrace();
                JOptionPane.showMessageDialog(null, "无法启动QQ，可能是人为因素。",
                        "内部发生异常！", JOptionPane.ERROR_MESSAGE);
            } catch (Exception e) {
                System.err.println("Unhandled exception caught!");
                e.printStackTrace();
                JOptionPane.showMessageDialog(null, "内部错误：" + e.getClass().getSimpleName() + ", 详询日志消息。",
                        "内部发生异常！", JOptionPane.ERROR_MESSAGE);
                System.exit(1);
            }
        } catch (HeadlessException e) {
            System.err.println("Unsupported platform!");
            e.printStackTrace();
            System.exit(1);
        }
    }

    private enum ProcessState {
        PREPARING, DONE, TIMED_OUT, FAILED
    }
}
